DefaultEntityMapping.java
package org.codefilarete.stalactite.mapping;
import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import org.codefilarete.reflection.Accessor;
import org.codefilarete.reflection.AccessorChain;
import org.codefilarete.reflection.AccessorDefinition;
import org.codefilarete.reflection.Accessors;
import org.codefilarete.reflection.PropertyAccessor;
import org.codefilarete.reflection.ReversibleAccessor;
import org.codefilarete.reflection.ReversibleMutator;
import org.codefilarete.reflection.ValueAccessPoint;
import org.codefilarete.reflection.ValueAccessPointMap;
import org.codefilarete.stalactite.mapping.RowTransformer.TransformerListener;
import org.codefilarete.stalactite.mapping.id.assembly.SingleIdentifierAssembler;
import org.codefilarete.stalactite.mapping.id.manager.IdentifierInsertionManager;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.result.ColumnedRow;
import org.codefilarete.tool.Duo;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.collection.KeepOrderMap;
import org.codefilarete.tool.collection.KeepOrderSet;
import org.codefilarete.tool.function.Converter;
/**
* <p>
* Main class for persistence entity mapping description.
* Composed of:
* <ul>
* <li>a main strategy : an embedded one ({@link EmbeddedClassMapping}, accessible by {@link #getMainMapping()}</li>
* <li>an id strategy : {@link SimpleIdMapping} accessible with {@link #getIdMapping()}</li>
* <li>optional version mapping : accessible with {@link #getVersionedKeys()} for instance</li>
* <li>additional mappings (for embeddable for instance) : see {@link #put(ReversibleAccessor, EmbeddedBeanMapping)}</li>
* </ul>
* </p>
* <p>
* Note that mapping is defined through getter ({@link ReversibleAccessor}) and not setter ({@link ReversibleMutator}) only
* because a choice must be done between them, else the concrete class {@link PropertyAccessor} is the choice, which is less open.
* </p>
*
* <p>
* Mapping definition can be eased thanks to {@link PersistentFieldHarvester} or {@link Accessors}
* </p>
*
* <br/>
* <b>THIS CLASS DOESN'T ADDRESS RELATION MAPPING</b>, because it's not the purpose of Stalactite 'core' module, see 'orm' module.
* Meanwhile one can use {@link org.codefilarete.stalactite.query.model.Query} to construct complex type.
*
* @author Guillaume Mary
* @see org.codefilarete.stalactite.query.model.Query
*/
public class DefaultEntityMapping<C, I, T extends Table<T>> implements EntityMapping<C, I, T> {
private final EmbeddedClassMapping<C, T> mainMapping;
private final Set<Column<T, ?>> insertableColumns = new KeepOrderSet<>();
private final Set<Column<T, ?>> updatableColumns = new KeepOrderSet<>();
private final Set<Column<T, ?>> selectableColumns = new KeepOrderSet<>();
private final Map<ReversibleAccessor<C, ?>, EmbeddedBeanMapping<?, T>> embeddedMappings = new KeepOrderMap<>();
private final IdMapping<C, I> idMapping;
@Nullable
private final Duo<ReversibleAccessor<C, ?>, Column<T, ?>> versioningMapping;
private final boolean identifierSetByBeanFactory;
/**
* Main constructor
* Oriented for single column identifier / primary key. Prefer {@link #DefaultEntityMapping(Class, Table, Map, IdMapping)} for composed id.
* It only defines main class and table, secondary ones, such as embedded class, must be defined through {@link #put(ReversibleAccessor, EmbeddedBeanMapping)}
*
* @param classToPersist the class to be persisted
* @param targetTable the persisting table
* @param propertyToColumn mapping between bean "properties" and table columns
* @param identifierProperty identifier of the persisted class
* @param identifierInsertionManager manager of identifiers
* @see #put(ReversibleAccessor, EmbeddedBeanMapping)
*/
public DefaultEntityMapping(Class<C> classToPersist,
T targetTable,
Map<? extends ReversibleAccessor<C, ?>, ? extends Column<T, ?>> propertyToColumn,
ReversibleAccessor<C, I> identifierProperty,
IdentifierInsertionManager<C, I> identifierInsertionManager) {
if (identifierProperty == null) {
throw new UnsupportedOperationException("No identifier property for " + Reflections.toString(classToPersist));
}
if (targetTable.getPrimaryKey() == null) {
throw new UnsupportedOperationException("No primary key column defined for " + targetTable.getAbsoluteName());
}
this.mainMapping = new EmbeddedClassMapping<>(classToPersist, targetTable, propertyToColumn);
// identifierAccessor must be the same instance as those stored in propertyToColumn for Map.remove method used in foreach()
Column<T, I> identifierColumn = (Column<T, I>) propertyToColumn.get(identifierProperty);
if (identifierColumn == null) {
throw new IllegalArgumentException("Bean identifier '" + AccessorDefinition.toString(identifierProperty) + "' must have its matching column in the mapping");
}
if (!identifierColumn.isPrimaryKey()) {
throw new UnsupportedOperationException("Accessor '" + AccessorDefinition.toString(identifierProperty)
+ "' is declared as identifier but mapped column " + identifierColumn + " is not the primary key of table");
}
this.idMapping = new SimpleIdMapping<>(identifierProperty, identifierInsertionManager, new SingleIdentifierAssembler<>(identifierColumn));
this.identifierSetByBeanFactory = false;
this.versioningMapping = null;
fillInsertableColumns();
fillUpdatableColumns();
fillSelectableColumns();
}
/**
* Secondary constructor, for composed id because one can precisely define the {@link IdMapping} by giving a {@link ComposedIdMapping}
* for instance.
* It only defines main class and table, secondary ones, such as embedded class, must be defined through {@link #put(ReversibleAccessor, EmbeddedBeanMapping)}
*
* @param classToPersist the class to be persisted
* @param targetTable the persisting table
* @param propertyToColumn mapping between bean "properties" and table columns
* @param idMapping mapping strategy of class identifier
* @see #put(ReversibleAccessor, EmbeddedBeanMapping)
*/
public DefaultEntityMapping(Class<C> classToPersist,
T targetTable,
Map<? extends ReversibleAccessor<C, ?>, Column<T, ?>> propertyToColumn,
IdMapping<C, I> idMapping) {
this(classToPersist, targetTable, propertyToColumn, Collections.emptyMap(), null, idMapping, null, false);
}
/**
* Constructor that let caller give a bean factory. The bean factory is given a value provider of the form of a {@link Function<Column, Object>}
* that can be called to get values to build expected entity or determine the right type to be instantiated according to current row values.
*
* @param classToPersist the class to be persisted
* @param targetTable the persisting table
* @param propertyToColumn mapping between bean "properties" and table columns
* @param idMapping mapping strategy of class identifier
* @param entityFactory entity factory
* @see #put(ReversibleAccessor, EmbeddedBeanMapping)
*/
public DefaultEntityMapping(Class<C> classToPersist,
T targetTable,
Map<? extends ReversibleAccessor<C, ?>, ? extends Column<T, ?>> propertyToColumn,
Map<? extends ReversibleAccessor<C, ?>, ? extends Column<T, ?>> readonlyColumns,
@Nullable Duo<? extends ReversibleAccessor<C, ?>, ? extends Column<T, ?>> versioningMapping,
IdMapping<C, I> idMapping,
@Nullable Function<ColumnedRow, C> entityFactory,
boolean identifierSetByBeanFactory) {
if (idMapping.getIdAccessor() == null) {
throw new UnsupportedOperationException("No identifier property defined for " + Reflections.toString(classToPersist));
}
if (targetTable.getPrimaryKey() == null) {
throw new UnsupportedOperationException("No primary key column defined for " + targetTable.getAbsoluteName());
}
// adding versioning columns to properties for reading
Map<ReversibleAccessor<C, ?>, Column<T, ?>> readonlyPropertiesMapping = new KeepOrderMap<>(readonlyColumns);
if (versioningMapping != null) {
readonlyPropertiesMapping.put(versioningMapping.getLeft(), versioningMapping.getRight());
}
this.mainMapping = new EmbeddedClassMapping<>(classToPersist, targetTable, propertyToColumn, readonlyPropertiesMapping, entityFactory);
this.idMapping = idMapping;
this.identifierSetByBeanFactory = identifierSetByBeanFactory;
this.versioningMapping = (Duo<ReversibleAccessor<C, ?>, Column<T, ?>>) versioningMapping;
fillInsertableColumns();
fillUpdatableColumns();
fillSelectableColumns();
}
public Class<C> getClassToPersist() {
return mainMapping.getClassToPersist();
}
@Override
public T getTargetTable() {
return mainMapping.getTargetTable();
}
public EmbeddedClassMapping<C, T> getMainMapping() {
return mainMapping;
}
@Override
public Map<ReversibleAccessor<C, ?>, EmbeddedBeanMapping<?, T>> getEmbeddedBeanStrategies() {
return embeddedMappings;
}
/**
* Implementation which returns all properties mapping, even embedded ones.
* The Result is built dynamically.
*
* @return all properties mapping, even embedded ones
*/
@Override
public Map<ReversibleAccessor<C, ?>, Column<T, ?>> getPropertyToColumn() {
Map<ReversibleAccessor<C, ?>, Column<T, ?>> result = new KeepOrderMap<>();
result.putAll(getMainMapping().getPropertyToColumn());
for (Entry<ReversibleAccessor<C, ?>, EmbeddedBeanMapping<?, T>> value : embeddedMappings.entrySet()) {
value.getValue().getPropertyToColumn().forEach((k, v) -> result.put(new AccessorChain<>(value.getKey(), k), v));
}
return result;
}
@Override
public Map<ReversibleAccessor<C, ?>, Column<T, ?>> getReadonlyPropertyToColumn() {
Map<ReversibleAccessor<C, ?>, Column<T, ?>> result = new KeepOrderMap<>();
result.putAll(getMainMapping().getReadonlyPropertyToColumn());
for (Entry<ReversibleAccessor<C, ?>, EmbeddedBeanMapping<?, T>> value : embeddedMappings.entrySet()) {
value.getValue().getReadonlyPropertyToColumn().forEach((k, v) -> {
// code below is only to ensure that cast as Accessor will be fine in all circumstances
Accessor<?, ?> kAsAccessor;
if (k instanceof ReversibleMutator) {
kAsAccessor = ((ReversibleMutator<Object, Object>) k).toAccessor();
} else {
// This is not an absolute end : it would be sufficient if we could build an AccessorChain with last element
// being a Mutator, making created AccessorChain a Mutator (non reversible in our case) this should be implemented to avoid this exception
throw new UnsupportedOperationException("Given accessor " + AccessorDefinition.toString(k) + " can't be converted to an Accessor"
+ " to make it last element of Accessor chain");
}
result.put(new AccessorChain<C, Object>(value.getKey(), kAsAccessor).toMutator(), v);
});
}
return result;
}
@Override
@Nullable
public Duo<ReversibleAccessor<C, ?>, Column<T, ?>> getVersioningMapping() {
return versioningMapping;
}
@Override
public ValueAccessPointMap<C, Converter<Object, Object>> getReadConverters() {
return mainMapping.getReadConverters();
}
@Override
public ValueAccessPointMap<C, Converter<Object, Object>> getWriteConverters() {
return mainMapping.getWriteConverters();
}
/**
* Gives columns that can be inserted: columns minus generated keys
* @return columns of all mapping strategies without auto-generated keys, as an immutable {@link Set}
*/
@Override
public Set<Column<T, ?>> getInsertableColumns() {
return Collections.unmodifiableSet(insertableColumns);
}
/**
* Gives columns that can be updated: columns minus keys
* @return columns of all mapping strategies without getKey(), as an immutable {@link Set}
*/
@Override
public Set<Column<T, ?>> getUpdatableColumns() {
return Collections.unmodifiableSet(updatableColumns);
}
/**
* Gives columns that can be selected
* @return columns of all mapping strategies, as an immutable {@link Set}
*/
@Override
public Set<Column<T, ?>> getSelectableColumns() {
return Collections.unmodifiableSet(selectableColumns);
}
@Override
public IdMapping<C, I> getIdMapping() {
return idMapping;
}
public Collection<ShadowColumnValueProvider<C, T>> getShadowColumnsForInsert() {
return Collections.unmodifiableCollection(this.mainMapping.getShadowColumnsForInsert());
}
public Collection<ShadowColumnValueProvider<C, T>> getShadowColumnsForUpdate() {
return Collections.unmodifiableCollection(this.mainMapping.getShadowColumnsForUpdate());
}
public void addShadowColumns(DefaultEntityMapping<C, I, T> entityMapping) {
entityMapping.mainMapping.getShadowColumnsForInsert().forEach(this::addShadowColumnInsert);
entityMapping.mainMapping.getShadowColumnsForUpdate().forEach(this::addShadowColumnUpdate);
}
@Override
public void addShadowColumnInsert(ShadowColumnValueProvider<C, T> valueProvider) {
// we delegate value computation to the default mapping strategy
mainMapping.addShadowColumnInsert(valueProvider);
// we must register it as an insertable column so we'll generate the right SQL order
insertableColumns.addAll(valueProvider.getColumns());
}
@Override
public void addShadowColumnUpdate(ShadowColumnValueProvider<C, T> valueProvider) {
// we delegate value computation to the default mapping strategy
mainMapping.addShadowColumnUpdate(valueProvider);
// we must register it as an insertable column so we'll generate the right SQL order
updatableColumns.addAll(valueProvider.getColumns());
}
@Override
public void addPropertySetByConstructor(ValueAccessPoint<C> accessor) {
mainMapping.addPropertySetByConstructor(accessor);
}
@Override
public <O> void addShadowColumnSelect(Column<T, O> column) {
mainMapping.addShadowColumnSelect(column);
fillSelectableColumns();
}
/**
* Sets a particular strategy for a given property.
* The property type is supposed to be complex so it needs multiple columns for persistence and then needs an {@link EmbeddedBeanMapping}
*
* @param property an object representing a {@link Field} or {@link Method}
* @param mappingStrategy the strategy that should be used to persist the member
*/
public <O> void put(ReversibleAccessor<C, O> property, EmbeddedBeanMapping<O, T> mappingStrategy) {
embeddedMappings.put(property, mappingStrategy);
// update columns lists
addInsertableColumns(mappingStrategy);
addUpdatableColumns(mappingStrategy);
addSelectableColumns(mappingStrategy);
}
@Override
public Map<Column<T, ?>, ?> getInsertValues(C c) {
Map<Column<T, ?>, Object> insertValues = mainMapping.getInsertValues(c);
getVersionedKeyValues(c).entrySet().stream()
// autoincrement columns mustn't be written
.filter(entry -> !entry.getKey().isAutoGenerated())
.forEach(entry -> insertValues.put(entry.getKey(), entry.getValue()));
this.foreachMappedField(mappingEntry -> {
Object fieldValue = mappingEntry.getKey().get(c);
Map<Column<T, ?>, ?> fieldInsertValues = mappingEntry.getValue().getInsertValues(fieldValue);
insertValues.putAll(fieldInsertValues);
});
return insertValues;
}
@Override
public Map<UpwhereColumn<T>, ?> getUpdateValues(C modified, C unmodified, boolean allColumns) {
Map<UpwhereColumn<T>, Object> toReturn;
if (modified != null && unmodified != null && !getId(modified).equals(getId(unmodified))) {
// entities are different, so there's no value to be updated
toReturn = new HashMap<>();
} else {
toReturn = mainMapping.getUpdateValues(modified, unmodified, allColumns);
this.foreachMappedField(mappingEntry -> {
ReversibleAccessor<C, ?> accessor = mappingEntry.getKey();
Object modifiedValue = accessor.get(modified);
Object unmodifiedValue = unmodified == null ? null : accessor.get(unmodified);
Map<UpwhereColumn<T>, ?> fieldUpdateValues = mappingEntry.getValue().getUpdateValues(modifiedValue, unmodifiedValue, allColumns);
toReturn.putAll(fieldUpdateValues);
});
if (!toReturn.isEmpty()) {
if (allColumns) {
Set<Column<T, ?>> missingColumns = new HashSet<>(getUpdatableColumns());
missingColumns.removeAll(UpwhereColumn.getUpdateColumns(toReturn).keySet());
for (Column<T, ?> missingColumn : missingColumns) {
toReturn.put(new UpwhereColumn<>(missingColumn, true), null);
}
}
// Determining on which instance we should take where values : unmodified by default, modified for rough update (unmodified is null)
C whereSource = unmodified != null ? unmodified : modified;
for (Entry<Column<T, ?>, ?> entry : getVersionedKeyValues(whereSource).entrySet()) {
toReturn.put(new UpwhereColumn<>(entry.getKey(), false), entry.getValue());
}
}
}
return toReturn;
}
/**
* Iterates over embeddedMappings field of this instance and call the given consumer for each of them.
* Made to deal with generics problems on caller (without it, code doesn't compile due to "?")
*
* @param consumer code that consumes each embeddedMappings entry
* @param <E> type of mapped beans
*/
private <E> void foreachMappedField(Consumer<Entry<ReversibleAccessor<C, E>, EmbeddedBeanMapping<E, T>>> consumer) {
embeddedMappings.entrySet().forEach((Consumer) consumer);
}
/**
* Build columns that can be inserted: columns minus generated keys
*/
private void fillInsertableColumns() {
insertableColumns.clear();
addInsertableColumns(mainMapping);
insertableColumns.addAll(getIdMapping().<T>getIdentifierAssembler().getColumns());
embeddedMappings.values().forEach(this::addInsertableColumns);
if (versioningMapping != null) {
insertableColumns.add(versioningMapping.getRight());
}
// NB : generated keys are never inserted but left because DMLGenerator needs its presence to detect it
// and also it prevents to generate empty statement when they are Alone in the Dark ;)
}
private void addInsertableColumns(EmbeddedBeanMapping<?, T> embeddedBeanMapping) {
insertableColumns.addAll(embeddedBeanMapping.getWritableColumns());
}
/**
* Build columns that can be updated: columns minus keys
*/
private void fillUpdatableColumns() {
updatableColumns.clear();
addUpdatableColumns(mainMapping);
embeddedMappings.values().forEach(this::addUpdatableColumns);
if (versioningMapping != null) {
updatableColumns.add(versioningMapping.getRight());
}
// keys are never updated
updatableColumns.removeAll(getTargetTable().getPrimaryKey().getColumns());
updatableColumns.removeIf(Column::isAutoGenerated);
}
private void addUpdatableColumns(EmbeddedBeanMapping<?, T> embeddedBeanMapping) {
updatableColumns.addAll(embeddedBeanMapping.getWritableColumns());
}
private void fillSelectableColumns() {
selectableColumns.clear();
addSelectableColumns(mainMapping);
selectableColumns.addAll(getIdMapping().<T>getIdentifierAssembler().getColumns());
if (versioningMapping != null) {
selectableColumns.add(versioningMapping.getRight());
}
embeddedMappings.values().forEach(this::addSelectableColumns);
}
private void addSelectableColumns(EmbeddedBeanMapping<?, T> embeddedBeanMapping) {
selectableColumns.addAll(embeddedBeanMapping.getColumns());
}
@Override
public Map<Column<T, ?>, ?> getVersionedKeyValues(C c) {
Map<Column<T, ?>, Object> toReturn = new HashMap<>();
toReturn.putAll(getIdMapping().<T>getIdentifierAssembler().getColumnValues(getId(c)));
if (versioningMapping != null) {
toReturn.put(versioningMapping.getRight(), versioningMapping.getLeft().get(c));
}
return toReturn;
}
@Override
public Iterable<Column<T, ?>> getVersionedKeys() {
// we return a stable set to keep tests stable, shouldn't impact performances
Set<Column<T, ?>> columns = new KeepOrderSet<>();
if (versioningMapping != null) {
columns.add(versioningMapping.getRight());
}
columns.addAll(getTargetTable().getPrimaryKey().getColumns());
return Collections.unmodifiableSet(columns);
}
@Override
public I getId(C c) {
return getIdMapping().getIdAccessor().getId(c);
}
@Override
public void setId(C c, I identifier) {
getIdMapping().getIdAccessor().setId(c, identifier);
}
@Override
public boolean isNew(C c) {
return getIdMapping().isNew(c);
}
@Override
public C transform(ColumnedRow row) {
return getRowTransformer().transform(row);
}
@Override
public RowTransformer<C> getRowTransformer() {
return new RowTransformer<C>() {
@Override
public C transform(ColumnedRow row) {
C toReturn = mainMapping.getRowTransformer().transform(row);
// fixing identifier
// Note : this may be done twice in single column primary key case, because constructor expects that the column must be present in the
// mapping, then it is used by the SingleIdentifierAssembler
if (!identifierSetByBeanFactory) {
setId(toReturn, getIdMapping().getIdentifierAssembler().assemble(row));
}
// filling other properties
foreachMappedField(mappingEntry -> {
mappingEntry.getKey().toMutator().set(toReturn, mappingEntry.getValue().transform(row));
});
return toReturn;
}
@Override
public C newBeanInstance(ColumnedRow row) {
return mainMapping.transform(row);
}
@Override
public void applyRowToBean(ColumnedRow row, C bean) {
mainMapping.getRowTransformer().applyRowToBean(row, bean);
}
@Override
public void addTransformerListener(TransformerListener<? extends C> listener) {
mainMapping.addTransformerListener((TransformerListener<C>) listener);
}
};
}
@Override
public void addTransformerListener(TransformerListener<C> listener) {
getRowTransformer().addTransformerListener(listener);
}
}